﻿''' <summary>
''' Class to handle mapping of two tables with station/pcode aggregation and temporal aggregation
''' </summary>
''' <remarks></remarks>
Public Class clsCalibration

    ''' <summary>
    ''' Name of calibration dataset
    ''' </summary>
    Friend Property Name As String

    ''' <summary>
    ''' Table associated with measured data (e.g., .db table)
    ''' </summary>
    Friend Property tblMeasured As clsTable

    ''' <summary>
    ''' Table associated with simulated data (e.g., .bmd table)
    ''' </summary>
    Friend Property tblSimulated As clsTable

    ''' <summary>
    ''' Name of parameter to use for spatial aggregation weight
    ''' </summary>
    Friend Property WeightingParameter As String = ""

    ''' <summary>
    ''' Minimum date to use for calibration report
    ''' </summary>
    Friend Property MinDate As Date = Now.Date

    ''' <summary>
    ''' Maximum date to use for calibration report
    ''' </summary>
    Friend Property MaxDate As Date = Now.Date

    ''' <summary>
    ''' Flag to determine if new series are assigned series names automatically
    ''' </summary>
    Friend Property AutoSeriesPrefix As Boolean = False

    ''' <summary>
    ''' If AutoSeriesPrefix is false, will use this string when creating new series
    ''' </summary>
    Friend Property SeriesPrefix As String = ""

    ''' <summary>
    ''' Suffix to add to legend name for measured curve
    ''' </summary>
    Friend Property LegendSuffixMeasured As String = "Measured"

    ''' <summary>
    ''' Suffix to add to legend name for simulated curve
    ''' </summary>
    Friend Property LegendSuffixSimulated As String = "Simulated"

    Friend Enum enumTimeAgg
        None
        Hour
        Day
        Week
        Month
    End Enum

    Friend Enum enumDataType
        Measured
        Simulated
    End Enum

    Friend Enum enumPercentiles
        _5_95
        _10_90
        _25_75
    End Enum

    ''' <summary>
    ''' Period of time over which data are to be aggregated
    ''' </summary>
    Friend Property TimeAggregation As enumTimeAgg = enumTimeAgg.None

    ''' <summary>
    ''' Preferred percentiles for reporting
    ''' </summary>
    Friend Property PreferredPercentiles As enumPercentiles = enumPercentiles._5_95

    ''' <summary>
    ''' Return the preferred lower percentile value from the array
    ''' </summary>
    ''' <param name="Percentiles">Standard 19-element percentile array in 5 percent increments</param>
    Friend Function PercentileLo(Percentiles() As Double) As Double
        Select Case PreferredPercentiles
            Case enumPercentiles._5_95 : Return Percentiles(0)
            Case enumPercentiles._10_90 : Return Percentiles(1)
            Case enumPercentiles._25_75 : Return Percentiles(4)
            Case Else : Return 0.0
        End Select
    End Function

    ''' <summary>
    ''' Return the preferred higher percentile value from the array
    ''' </summary>
    ''' <param name="Percentiles">Standard 19-element percentile array in 5 percent increments</param>
    Friend Function PercentileHi(Percentiles() As Double) As Double
        Select Case PreferredPercentiles
            Case enumPercentiles._5_95 : Return Percentiles(18)
            Case enumPercentiles._10_90 : Return Percentiles(17)
            Case enumPercentiles._25_75 : Return Percentiles(14)
            Case Else : Return 0.0
        End Select
    End Function

    ''' <summary>
    ''' Return the preferred lower percentile string
    ''' </summary>
    Friend Function PercentileStringLo() As String
        Select Case PreferredPercentiles
            Case enumPercentiles._5_95 : Return "5"
            Case enumPercentiles._10_90 : Return "10"
            Case enumPercentiles._25_75 : Return "25"
            Case Else : Return ""
        End Select
    End Function

    ''' <summary>
    ''' Return the preferred lower percentile string
    ''' </summary>
    Friend Function PercentileStringHi() As String
        Select Case PreferredPercentiles
            Case enumPercentiles._5_95 : Return "95"
            Case enumPercentiles._10_90 : Return "90"
            Case enumPercentiles._25_75 : Return "75"
            Case Else : Return ""
        End Select
    End Function

    ''' <summary>
    ''' Each branch can have multiple stations assigned to it; these
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Friend Property dictBranches As New Generic.Dictionary(Of String, clsBranch)

    Sub New(CalibrationName As String)
        Name = CalibrationName
        dictBranches.Add("Default", New clsBranch("Default"))
        For Each tbl As clsTable In OpenTables
            If tbl.TableType = clsTable.enumTableType.BMD Then
                tblSimulated = tbl
                Exit For
            End If
        Next

        For Each tbl As clsTable In OpenTables
            If tbl.TableType <> clsTable.enumTableType.BMD Then
                tblMeasured = tbl
                Exit For
            End If
        Next
    End Sub

    ''' <summary>
    ''' Get values from measured or simulated values table, aggregated for all mapped stations and using time aggregation
    ''' Return in DataTable having two columns: Date_Time and Result
    ''' </summary>
    Friend Function GetData(StationAgg As String, ParmAgg As String, GetDataType As enumDataType) As DataTable
        Try
            Dim dtResultWt As New DataTable()
            dtResultWt.Columns.Add("Date_Time", GetType(Date))
            dtResultWt.Columns.Add("Result", GetType(Double))
            dtResultWt.Columns.Add("Weight", GetType(Double))

            Dim dtResultAgg As New DataTable()
            dtResultAgg.Columns.Add("Date_Time", GetType(Date))
            dtResultAgg.Columns.Add("Result", GetType(Double))

            'for measured data, just average all values; for simulated do weighted average (based on cell volume)

            Dim WeightVar As String = ""
            If GetDataType = enumDataType.Simulated AndAlso
                Not String.IsNullOrEmpty(WeightingParameter) AndAlso
                tblSimulated.TableType = clsTable.enumTableType.BMD Then
                With tblSimulated.dsBMD
                    For v As Integer = 0 To .NumVars - 1
                        If .VarName(v) = "Bot " & WeightingParameter OrElse
                           .VarName(v) Like "K=?? " & WeightingParameter OrElse
                           .VarName(v) Like "Surf " & WeightingParameter OrElse
                           .VarName(v) = WeightingParameter Then
                            WeightVar = .VarName(v)
                            Exit For
                        End If
                    Next
                End With
            End If

            'find first branch containing specified station and parameter; for all stations and parameters mapped there, accumulate in data table for later processing

            For Each b As clsBranch In dictBranches.Values
                If b.dictStations.ContainsKey(StationAgg) AndAlso b.dictStations(StationAgg).dictParameters.ContainsKey(ParmAgg) Then
                    With b.dictStations(StationAgg).dictParameters(ParmAgg)
                        For Each s As String In IIf(GetDataType = enumDataType.Measured, .lstStationsMeas, .lstStationsSim)
                            For Each p As String In IIf(GetDataType = enumDataType.Measured, .lstParametersMeas, .lstParametersSim)
                                Dim dtResultTemp As DataTable = CType(IIf(GetDataType = enumDataType.Measured, tblMeasured, tblSimulated), clsTable).GetResults(s, p, , , .MinDepth, .MaxDepth)
                                If dtResultTemp IsNot Nothing Then
                                    For Each r As DataRow In dtResultTemp.Rows
                                        Dim dt As Date = r(0)
                                        If dt >= MinDate AndAlso dt <= MaxDate Then
                                            Dim result As Double = r(1)

                                            'for measured data, just average all values; for simulated do weighted average (based on cell volume)
                                            Dim wt As Double = 1
                                            If WeightVar <> "" Then wt = tblSimulated.dsBMD.Conc(WeightVar, s, tblSimulated.dsBMD.TimeIndex(dt))
                                            dtResultWt.Rows.Add(dt, result, wt)
                                        End If
                                    Next
                                End If
                            Next
                        Next
                    End With

                    If dtResultWt.Rows.Count = 0 Then Return dtResultAgg 'return empty table

                    dtResultWt.DefaultView.Sort = "Date_Time"

                    'average all observations within each aggregated time step
                    Dim MinDateTime As Date = dtResultWt.DefaultView(0)(0)

                    'reset minimum date to even value depending on aggregation length
                    With MinDateTime
                        Select Case TimeAggregation
                            Case enumTimeAgg.Hour : MinDateTime = New Date(.Year, .Month, .Day, .Hour, 0, 0)
                            Case enumTimeAgg.Day : MinDateTime = New Date(.Year, .Month, .Day)
                            Case enumTimeAgg.Week
                                MinDateTime = New Date(.Year, .Month, .Day)
                                MinDateTime = MinDateTime.AddDays(-MinDateTime.DayOfWeek) 'change to first day of week (sunday)
                            Case enumTimeAgg.Month : MinDateTime = New Date(.Year, .Month, 1)
                        End Select
                    End With

                    Dim i As Integer = 0
                    Dim ThisDate As Date = MinDateTime
                    Dim NextDate As Date = MinDateTime 'will immediately increment below
                    Dim SumResults As Double = 0
                    Dim SumWeights As Double = 0
                    Dim MinDateAgg As Date = Date.MaxValue
                    Dim MaxDateAgg As Date = Date.MinValue

                    Do
                        Select Case TimeAggregation
                            Case enumTimeAgg.None : NextDate = NextDate.AddSeconds(0.1) 'only aggregates in space, not time
                            Case enumTimeAgg.Hour : NextDate = NextDate.AddHours(1)
                            Case enumTimeAgg.Day : NextDate = NextDate.AddDays(1)
                            Case enumTimeAgg.Week : NextDate = NextDate.AddDays(7)
                            Case enumTimeAgg.Month : NextDate = NextDate.AddMonths(1)
                        End Select

                        Do
                            With dtResultWt.DefaultView.Item(i)
                                ThisDate = .Item(0)
                                Dim result As Double = .Item(1)
                                Dim wt As Double = .Item(2)

                                If TimeAggregation <> enumTimeAgg.None AndAlso ThisDate >= NextDate Then Exit Do

                                SumResults += result * wt
                                SumWeights += wt 'for measured not using weighted averages
                                If ThisDate < MinDateAgg Then MinDateAgg = ThisDate
                                If ThisDate > MaxDateAgg Then MaxDateAgg = ThisDate

                                i += 1
                                If i > dtResultWt.Rows.Count - 1 Or TimeAggregation = enumTimeAgg.None Then
                                    Exit Do
                                End If
                            End With
                        Loop

                        If SumWeights <> 0 Then
                            'set new data date/time to middle of the range of observations in the period
                            Dim AggDate As Date = MinDateAgg.AddDays(MaxDateAgg.Subtract(MinDateAgg).TotalDays / 2)

                            Dim NewValue As Double = IIf(SumWeights = 0, Double.MaxValue, SumResults / SumWeights)
                            dtResultAgg.Rows.Add(AggDate, NewValue)
                        End If

                        SumResults = 0
                        SumWeights = 0
                        MinDateAgg = Date.MaxValue
                        MaxDateAgg = Date.MinValue

                    Loop Until i > dtResultWt.Rows.Count - 1

                    Return dtResultAgg

                End If
            Next

            Return Nothing

        Catch ex As Exception
            ErrorMsg(, ex)
            Return Nothing
        End Try
    End Function

    Public Sub SaveSettings(iniFile As WRDB.IniFile.clsIniFile, SectionName As String)
        iniFile.SetKeyText(SectionName, "CalibrationName", Name)
        iniFile.SetKeyText(SectionName, "TimeAggregation", TimeAggregation)
        iniFile.SetKeyText(SectionName, "PreferredPercentiles", PreferredPercentiles)
        iniFile.SetKeyText(SectionName, "WeightingParameter", WeightingParameter)
        iniFile.SetKeyText(SectionName, "MinDate", MinDate.ToString("MM/dd/yyyy"))
        iniFile.SetKeyText(SectionName, "MaxDate", MaxDate.ToString("MM/dd/yyyy"))
        iniFile.SetKeyText(SectionName, "AutoSeriesPrefix", AutoSeriesPrefix)
        iniFile.SetKeyText(SectionName, "SeriesPrefix", SeriesPrefix)
        iniFile.SetKeyText(SectionName, "LegendSuffixMeasured", LegendSuffixMeasured)
        iniFile.SetKeyText(SectionName, "LegendSuffixSimulated", LegendSuffixSimulated)

        tblMeasured.SaveSettings(iniFile, SectionName & "_Meas")
        tblSimulated.SaveSettings(iniFile, SectionName & "_Sim")

        iniFile.SetKeyText(SectionName, "Branches", ListToString(New Generic.List(Of String)(dictBranches.Keys)))

        For Each b As clsBranch In dictBranches.Values
            Dim SectionNameBranch As String = SectionName & "_Branch_" & b.Name
            iniFile.SetKeyText(SectionNameBranch, "Stations", ListToString(New Generic.List(Of String)(b.dictStations.Keys)))
            For Each sta As clsStation In b.dictStations.Values
                Dim SectionNameStation As String = SectionNameBranch & "_" & sta.Name
                iniFile.SetKeyText(SectionNameStation, "Length", sta.Length)
                iniFile.SetKeyText(SectionNameStation, "Parameters", ListToString(New Generic.List(Of String)(sta.dictParameters.Keys)))
                For Each parm As clsParameter In sta.dictParameters.Values
                    Dim SectionNameParm As String = SectionNameStation & "_" & parm.Name
                    For i As Integer = 1 To 4
                        Dim Key As String = Choose(i, "StationsMeas", "ParametersMeas", "StationsSim", "ParametersSim")
                        Dim lst As Generic.List(Of String) = Choose(i, parm.lstStationsMeas, parm.lstParametersMeas, parm.lstStationsSim, parm.lstParametersSim)
                        iniFile.SetKeyText(SectionNameParm, Key, ListToString(lst))
                    Next
                    iniFile.SetKeyText(SectionNameParm, "MinDepth", IIf(parm.MinDepth = Double.MinValue, "", parm.MinDepth))
                    iniFile.SetKeyText(SectionNameParm, "MaxDepth", IIf(parm.MaxDepth = Double.MaxValue, "", parm.MaxDepth))
                Next
            Next
        Next
    End Sub

    Public Sub LoadSettings(iniFile As WRDB.IniFile.clsIniFile, SectionName As String)
        Name = iniFile.GetKeyText(SectionName, "CalibrationName")
        TimeAggregation = iniFile.GetKeyText(SectionName, "TimeAggregation", enumTimeAgg.None)
        PreferredPercentiles = iniFile.GetKeyText(SectionName, "PreferredPercentiles", enumPercentiles._5_95)
        WeightingParameter = iniFile.GetKeyText(SectionName, "WeightingParameter", "")
        MinDate = iniFile.GetKeyText(SectionName, "MinDate", Now.Date)
        MaxDate = iniFile.GetKeyText(SectionName, "MaxDate", Now.Date)
        AutoSeriesPrefix = iniFile.GetKeyText(SectionName, "AutoSeriesPrefix", False)
        SeriesPrefix = iniFile.GetKeyText(SectionName, "SeriesPrefix", "")
        LegendSuffixMeasured = iniFile.GetKeyText(SectionName, "LegendSuffixMeasured", "Measured")
        LegendSuffixSimulated = iniFile.GetKeyText(SectionName, "LegendSuffixSimulated", "Simulated")

        tblMeasured = New clsTable
        tblMeasured.LoadSettings(iniFile, SectionName & "_Meas")
        tblSimulated = New clsTable
        tblSimulated.LoadSettings(iniFile, SectionName & "_Sim")

        dictBranches.Clear()
        For Each BranchName As String In StringToList(iniFile.GetKeyText(SectionName, "Branches", ""))
            Dim SectionNameBranch As String = SectionName & "_Branch_" & BranchName
            Dim b As New clsBranch(BranchName)
            For Each StationName As String In StringToList(iniFile.GetKeyText(SectionNameBranch, "Stations", ""))
                Dim SectionNameStation As String = SectionNameBranch & "_" & StationName
                Dim sta As New clsStation(StationName)
                sta.Length = iniFile.GetKeyText(SectionNameStation, "Length")
                For Each ParmName As String In StringToList(iniFile.GetKeyText(SectionNameStation, "Parameters"))
                    Dim SectionNameParm As String = SectionNameStation & "_" & ParmName
                    Dim parm As New clsParameter(ParmName)
                    For i As Integer = 1 To 4
                        Dim Key As String = Choose(i, "StationsMeas", "ParametersMeas", "StationsSim", "ParametersSim")
                        Dim lst As Generic.List(Of String) = Choose(i, parm.lstStationsMeas, parm.lstParametersMeas, parm.lstStationsSim, parm.lstParametersSim)
                        For Each s As String In StringToList(iniFile.GetKeyText(SectionNameParm, Key, ""))
                            lst.AddRange(StringToList(s).ToArray)
                        Next
                    Next
                    parm.MinDepth = TestNull(iniFile.GetKeyText(SectionNameParm, "MinDepth", ""), Double.MinValue)
                    parm.MaxDepth = TestNull(iniFile.GetKeyText(SectionNameParm, "MaxDepth", ""), Double.MaxValue)
                    sta.dictParameters.Add(parm.Name, parm)
                Next
                b.dictStations.Add(sta.Name, sta)
            Next
            dictBranches.Add(b.Name, b)
        Next
    End Sub

    ''' <summary>
    ''' Structure to return computed calibration statistics
    ''' </summary>
    Public Structure structStats
        Dim Count As Integer
        Dim Mean As Double
        Dim Sum As Double
        Dim StdDev As Double
        Dim Min As Double
        Dim Max As Double
        Dim PercentileLow As Double
        Dim PercentileHigh As Double
        Dim Percentiles() As Double
    End Structure

    Public Sub CalcStats(dtMeas As DataTable, dtSim As DataTable, ByRef Stats() As structStats, ByRef NumPairs As Integer, ByRef R2 As Double)
        ReDim Stats(1)
        For i As enumDataType = enumDataType.Measured To enumDataType.Simulated
            Dim lst As New Generic.List(Of Double)
            Dim dt As DataTable = Choose(i + 1, dtMeas, dtSim)
            If dt IsNot Nothing Then
                For r As Integer = 0 To dt.Rows.Count - 1
                    lst.Add(dt.Rows(r)(1))
                Next
            End If
            With Stats(i)
                modProbability.CalcStats(lst.ToArray, .Sum, .Mean, .StdDev, .Count, .Min, .Max)
                modProbability.CalcStats(lst.ToArray, .Percentiles)
                .PercentileLow = PercentileLo(.Percentiles)
                .PercentileHigh = PercentileHi(.Percentiles)
            End With
        Next

        'calculate correlation coefficient using same approach (TimeWindow) used to plot series
        Dim lstX As New Generic.List(Of Double)
        Dim lstY As New Generic.List(Of Double)
        Dim DtaY As New Generic.SortedList(Of Date, Double)
        Dim dtX, dtY As DataTable
        If dtMeas IsNot Nothing AndAlso dtSim IsNot Nothing Then
            dtY = dtMeas
            dtX = dtSim
            For Each r As DataRow In dtY.Rows
                If Not DtaY.ContainsKey(r(0)) Then DtaY.Add(r(0), r(1))
                Application.DoEvents()
            Next
            For Each r As DataRow In dtX.Rows
                Dim tx As Date = r(0)
                Dim TimeWindowMin As Integer = 0
                Select Case TimeAggregation
                    Case enumTimeAgg.Hour : TimeWindowMin = 60
                    Case enumTimeAgg.Day : TimeWindowMin = 60 * 24
                    Case enumTimeAgg.Week : TimeWindowMin = 60 * 24 * 7
                    Case enumTimeAgg.Month : TimeWindowMin = 60 * 24 * 30
                End Select
                Dim txmin As Date = tx.AddMinutes(-TimeWindowMin)
                Dim txmax As Date = tx.AddMinutes(TimeWindowMin)
                Dim vx As Double = r(1)
                'find closest y value within the time tolerance
                Dim MinDiff As Double = Double.MaxValue, ClosestResult As Double = Double.MinValue
                For Each ty As Date In DtaY.Keys
                    If ty >= txmin AndAlso ty <= txmax Then
                        Dim diff As Double = Math.Abs(tx.Subtract(ty).TotalDays)
                        If diff < MinDiff Then
                            MinDiff = diff
                            ClosestResult = DtaY(ty)
                        End If
                    End If
                    If ty > txmax Then Exit For
                Next
                If MinDiff <> Double.MaxValue Then
                    lstX.Add(vx)
                    lstY.Add(ClosestResult)
                End If
            Next

            Dim arResultPairs(1, lstX.Count - 1) As Double
            For i As Integer = 0 To lstX.Count - 1
                arResultPairs(0, i) = lstX(i)
                arResultPairs(1, i) = lstY(i)
            Next
            Dim FitStats As structFitStats
            If modProbability.ComputeLeastSquares(arResultPairs, enumCurveFit.Linear, FitStats) Then
                NumPairs = FitStats.NumPoints
                R2 = FitStats.CorrCoef ^ 2
            End If
        End If
    End Sub

    Public Sub CalcStats(Station As String, Parameter As String, ByRef Stats() As structStats, ByRef NumPairs As Integer, ByRef R2 As Double)
        CalcStats(GetData(Station, Parameter, enumDataType.Measured), GetData(Station, Parameter, enumDataType.Simulated), Stats, NumPairs, R2)
    End Sub

    ''' <summary>
    ''' Create short report for single station and parameter or long report for all stations and parameter
    ''' </summary>
    ''' <param name="Station">Name of Aggregate Station or (All Stations...</param>
    ''' <param name="Parameter">Name of Aggregate Parameter</param>
    ''' <returns>HTML formatted report</returns>
    Public Function CreateReport(Station As String, Parameter As String, Optional FontSize As HTMLBuilder.clsHTMLBuilder.enumFontSize = HTMLBuilder.clsHTMLBuilder.enumFontSize.Medium) As String
        Try
            WriteProgress(GraphForm, "Generating report...")

            Dim lstSta As New Generic.List(Of String)
            If Station.StartsWith("(All") Then
                For Each b As clsBranch In dictBranches.Values
                    For Each sta As String In b.dictStations.Keys
                        If Not lstSta.Contains(sta) Then lstSta.Add(sta)
                    Next
                Next
                lstSta.Sort()
            Else
                lstSta.Add(Station)
            End If

            Dim isShortRpt As Boolean = lstSta.Count = 1

            With New HTMLBuilder.clsHTMLBuilder
                .AppendFont(FontSize, HTMLBuilder.clsHTMLBuilder.enumColors.black)
                If isShortRpt Then
                    .AppendPara(HTMLBuilder.clsHTMLBuilder.enumAlign.Center, "Calibration Statistics")
                    .AppendPara(HTMLBuilder.clsHTMLBuilder.enumAlign.Center, String.Format("Station: {0}; Parameter: {1}", Station, Parameter))
                    .AppendTable(, , , , , HTMLBuilder.clsHTMLBuilder.enumAlign.Center)
                    .AppendTableColumn("Statistic", , HTMLBuilder.clsHTMLBuilder.enumAlign.Center)
                    .AppendTableColumn(LegendSuffixSimulated, , HTMLBuilder.clsHTMLBuilder.enumAlign.Center)
                    .AppendTableColumn(LegendSuffixMeasured, , HTMLBuilder.clsHTMLBuilder.enumAlign.Center)
                Else
                    .AppendPara(HTMLBuilder.clsHTMLBuilder.enumAlign.Center, "Calibration Statistics")
                    .AppendPara(HTMLBuilder.clsHTMLBuilder.enumAlign.Center, String.Format("Parameter: {0}; Date Range: {1:MM/dd/yyyy} - {2:MM/dd/yyyy}", Parameter, MinDate, MaxDate))
                    .AppendTable(, , , , , HTMLBuilder.clsHTMLBuilder.enumAlign.Center)
                    .AppendTableRow()
                    .AppendTableColumnHeading("Station", 1, 2)
                    .AppendTableColumnHeading(LegendSuffixSimulated, 3, 1)
                    .AppendTableColumnHeading(LegendSuffixMeasured, 3, 1)
                    .AppendTableColumnHeading("Difference", 3, 1)
                    .AppendTableColumnHeading("R^2", 1, 2)
                    .AppendTableRowEnd()
                    .AppendTableColumn("", , HTMLBuilder.clsHTMLBuilder.enumAlign.Center)
                    .AppendTableColumn("Mean", , HTMLBuilder.clsHTMLBuilder.enumAlign.Center, 10, HTMLBuilder.clsHTMLBuilder.enumWidthUnits.Percent)
                    .AppendTableColumn(PercentileStringLo() & " %tile", , HTMLBuilder.clsHTMLBuilder.enumAlign.Center, 10, HTMLBuilder.clsHTMLBuilder.enumWidthUnits.Percent)
                    .AppendTableColumn(PercentileStringHi() & " %tile", , HTMLBuilder.clsHTMLBuilder.enumAlign.Center, 10, HTMLBuilder.clsHTMLBuilder.enumWidthUnits.Percent)
                    .AppendTableColumn("Mean", , HTMLBuilder.clsHTMLBuilder.enumAlign.Center, 10, HTMLBuilder.clsHTMLBuilder.enumWidthUnits.Percent)
                    .AppendTableColumn(PercentileStringLo() & " %tile", , HTMLBuilder.clsHTMLBuilder.enumAlign.Center, 10, HTMLBuilder.clsHTMLBuilder.enumWidthUnits.Percent)
                    .AppendTableColumn(PercentileStringHi() & " %tile", , HTMLBuilder.clsHTMLBuilder.enumAlign.Center, 10, HTMLBuilder.clsHTMLBuilder.enumWidthUnits.Percent)
                    .AppendTableColumn("Mean", , HTMLBuilder.clsHTMLBuilder.enumAlign.Center, 10, HTMLBuilder.clsHTMLBuilder.enumWidthUnits.Percent)
                    .AppendTableColumn(PercentileStringLo() & " %tile", , HTMLBuilder.clsHTMLBuilder.enumAlign.Center, 10, HTMLBuilder.clsHTMLBuilder.enumWidthUnits.Percent)
                    .AppendTableColumn(PercentileStringHi() & " %tile", , HTMLBuilder.clsHTMLBuilder.enumAlign.Center, 10, HTMLBuilder.clsHTMLBuilder.enumWidthUnits.Percent)
                    .AppendTableColumn("", , HTMLBuilder.clsHTMLBuilder.enumAlign.Center, 10, HTMLBuilder.clsHTMLBuilder.enumWidthUnits.Percent)
                End If

                For Each sta As String In lstSta
                    If Not isShortRpt Then WriteProgress(GraphForm, lstSta.IndexOf(sta), lstSta.Count - 1, "Analyzing data for {0}...", sta)

                    Dim Stats() As structStats = Nothing
                    Dim NumPairs As Integer, R2 As Double
                    CalcStats(sta, Parameter, Stats, NumPairs, R2)
                    If isShortRpt Then
                        .AppendTableRow("Count:", Stats(1).Count, Stats(0).Count)
                        .AppendTableRow("Mean:", Stats(1).Mean.ToString("N3"), Stats(0).Mean.ToString("N3"))
                        .AppendTableRow("Std Dev:", Stats(1).StdDev.ToString("N3"), Stats(0).StdDev.ToString("N3"))
                        .AppendTableRow("Min:", Stats(1).Min.ToString("N3"), Stats(0).Min.ToString("N3"))
                        .AppendTableRow("Max:", Stats(1).Max.ToString("N3"), Stats(0).Max.ToString("N3"))
                        .AppendTableRow("5 %tile:", Stats(1).Percentiles(0).ToString("N3"), Stats(0).Percentiles(0).ToString("N3"))
                        .AppendTableRow("10 %tile:", Stats(1).Percentiles(1).ToString("N3"), Stats(0).Percentiles(1).ToString("N3"))
                        .AppendTableRow("90 %tile:", Stats(1).Percentiles(17).ToString("N3"), Stats(0).Percentiles(17).ToString("N3"))
                        .AppendTableRow("95 %tile:", Stats(1).Percentiles(18).ToString("N3"), Stats(0).Percentiles(18).ToString("N3"))
                    Else
                        If Stats(0).Count = 0 Or Stats(1).Count = 0 Then Continue For 'if not matching data then don't display row at all
                        .AppendTableRow()
                        .AppendTableCell(sta)
                        .AppendTableCell(Stats(1).Mean.ToString("N3"))
                        .AppendTableCell(PercentileLo(Stats(1).Percentiles).ToString("N3"))
                        .AppendTableCell(PercentileHi(Stats(1).Percentiles).ToString("N3"))
                        .AppendTableCell(Stats(0).Mean.ToString("N3"))
                        .AppendTableCell(PercentileLo(Stats(0).Percentiles).ToString("N3"))
                        .AppendTableCell(PercentileHi(Stats(0).Percentiles).ToString("N3"))
                        .AppendTableCell((Stats(1).Mean - Stats(0).Mean).ToString("N3"))
                        .AppendTableCell((PercentileLo(Stats(1).Percentiles) - PercentileLo(Stats(0).Percentiles)).ToString("N3"))
                        .AppendTableCell((PercentileHi(Stats(1).Percentiles) - PercentileHi(Stats(0).Percentiles)).ToString("N3"))
                    End If

                    If isShortRpt Then
                        .AppendTableRow()
                        .AppendTableCell(HTMLBuilder.clsHTMLBuilder.enumAlign.Center, "Corr (R^2):")
                        .AppendTableCell(HTMLBuilder.clsHTMLBuilder.enumAlign.Center, 1, 2, String.Format("{0:N2}", R2))
                        .AppendTableRowEnd()
                    Else
                        .AppendTableCell(String.Format("{0:N2}", R2))
                        .AppendTableRowEnd()
                    End If
                Next
                .AppendTableEnd()
                .AppendFontEnd()
                Return .ToString
            End With
        Catch ex As Exception
            ErrorMsg(, ex)
            Return False
        Finally
            WriteProgress(GraphForm)
        End Try
    End Function

    ''' <summary>
    ''' Given semi-colon separated calibration string (such as stored in Graph file), try to parse it to return specifications
    ''' </summary>
    Public Shared Function TryParseCalibrationString(StringToParse As String, ByRef CalibrationName As String, ByRef Branch As String, ByRef Station As String, ByRef Parameter As String) As Boolean
        If Not StringToParse.StartsWith(CALIBRATIONSTRING) Then Return False
        Dim ar() As String = StringToParse.Split(";")
        If ar.Length = 5 Then
            CalibrationName = ar(1)
            Branch = ar(2)
            Station = ar(3)
            Parameter = ar(4)
            Return True
        Else
            Return False
        End If
    End Function

    ''' <summary>
    ''' Given calibration report specifications, return string suitable for saving in Graph file
    ''' </summary>
    ''' <param name="CalibrationName"></param>
    ''' <param name="Branch"></param>
    ''' <param name="Station"></param>
    ''' <param name="Parameter"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function GetCalibrationString(CalibrationName As String, Branch As String, Station As String, Parameter As String) As String
        Return ListToString(New String() {CALIBRATIONSTRING, CalibrationName, Branch, Station, Parameter})
    End Function

    ''' <summary>
    ''' Create a deep copy of instance
    ''' </summary>
    Public Function Clone() As clsCalibration
        Dim cal As New clsCalibration(Name)
        With cal
            .tblMeasured = tblMeasured
            .tblSimulated = tblSimulated
            .TimeAggregation = TimeAggregation
            .WeightingParameter = WeightingParameter
            .MinDate = MinDate
            .MaxDate = MaxDate
            .AutoSeriesPrefix = AutoSeriesPrefix
            .SeriesPrefix = SeriesPrefix
            .LegendSuffixMeasured = LegendSuffixMeasured
            .LegendSuffixSimulated = LegendSuffixSimulated
            .dictBranches = New Generic.Dictionary(Of String, clsBranch)
            For Each kv As Generic.KeyValuePair(Of String, clsBranch) In dictBranches
                .dictBranches.Add(kv.Key, kv.Value.Clone)
            Next
        End With
        Return cal
    End Function

End Class

''' <summary>
''' Class to hold branches comprised of lists stations logically associated with each other
''' </summary>
Friend Class clsBranch
    ''' <summary>Branch name</summary>
    Friend Property Name As String

    ''' <summary>Stations associated with branch</summary>
    Friend Property dictStations As New Generic.Dictionary(Of String, clsStation)

    Friend Sub New(BranchName As String)
        Name = BranchName
    End Sub

    Friend Function Clone() As clsBranch
        Dim b As New clsBranch(Name)
        With b
            .dictStations = New Generic.Dictionary(Of String, clsStation)
            For Each kv As Generic.KeyValuePair(Of String, clsStation) In dictStations
                .dictStations.Add(kv.Key, kv.Value.Clone)
            Next
        End With
        Return b
    End Function
End Class

''' <summary>
''' Class to hold mapped station defining station location and list of associated parameters
''' </summary>
Friend Class clsStation
    Friend Property Name As String
    Friend Property Length As Double
    Friend Property dictParameters As New Generic.Dictionary(Of String, clsParameter)

    Friend Sub New(StationName As String)
        Name = StationName
    End Sub

    Friend Function Clone() As clsStation
        Dim sta As New clsStation(Name)
        With sta
            .Length = Length
            .dictParameters = New Generic.Dictionary(Of String, clsParameter)
            For Each kv As Generic.KeyValuePair(Of String, clsParameter) In dictParameters
                .dictParameters.Add(kv.Key, kv.Value.Clone)
            Next
        End With
        Return sta
    End Function
End Class

''' <summary>
''' Class to hold mapped parameter defining list of associated aggregated measured and simulated stations and pcodes
''' </summary>
Friend Class clsParameter
    Friend Property Name As String
    Friend Property lstStationsMeas As New Generic.List(Of String)
    Friend Property lstParametersMeas As New Generic.List(Of String)
    Friend Property lstStationsSim As New Generic.List(Of String)
    Friend Property lstParametersSim As New Generic.List(Of String)

    ''' <summary>
    ''' Minimum depth from measured data table to include (must have DEPTH_M field)
    ''' </summary>
    Friend Property MinDepth As Double = Double.MinValue

    ''' <summary>
    ''' Maximum depth from measured data table to include (must have DEPTH_M field)
    ''' </summary>
    Friend Property MaxDepth As Double = Double.MaxValue

    Friend Sub New(ParmName As String)
        Name = ParmName
    End Sub

    Friend Function Clone() As clsParameter
        Dim parm As New clsParameter(Name)
        With parm
            .lstStationsMeas.AddRange(lstStationsMeas.GetRange(0, lstStationsMeas.Count))
            .lstParametersMeas.AddRange(lstParametersMeas.GetRange(0, lstParametersMeas.Count))
            .lstStationsSim.AddRange(lstStationsSim.GetRange(0, lstStationsSim.Count))
            .lstParametersSim.AddRange(lstParametersSim.GetRange(0, lstParametersSim.Count))
            .MinDepth = MinDepth
            .MaxDepth = MaxDepth
        End With
        Return parm
    End Function
End Class
